package dnl.jexem.cmd;

import java.util.ArrayList;
import java.util.List;

/**
 * A generic abstraction of a command execution.
 *
 * @author Daniel Orr
 *
 */
public abstract class CommandExecution {

    protected Command command;
    protected String workDir;
    protected int exitStatus;
    // keep this private so all status changes will be monitored.
    private ExecutionStatus executionStatus;
    protected TerminationReason terminationReason;
    protected final List<String> exceptionMessages = new ArrayList<>();
    private final List<ExecutionListener> executionListeners = new ArrayList<>();

    /**
     * Constructs a <code>CommandExecution</code> using strings.
     *
     * @param command the first <code>String</code> is assumed to be the command and all other strings are the
     * arguments.
     */
    public CommandExecution(String... command) {
        this.command = new Command(command);
        this.setWorkDir(".");
    }

    /**
     * Constructs a <code>CommandExecution</code> using a <code>Command</code> object.
     *
     * @param command a <code>Command</code> object.
     */
    public CommandExecution(Command command) {
        this.command = command;
    }

    public final void setWorkDir(String workDir) {
        this.workDir = workDir;
    }

    public final String getWorkDir() {
        return workDir;
    }

    public void addExecutionListener(ExecutionListener executionListener) {
        this.executionListeners.add(executionListener);
    }

    /**
     * Gets the full command and arguments.
     *
     * @return a <code>String</code> that is equivalent to the way the command is executed on the command
     * line.
     */
    public final String getFullCommand() {
        return command.getFullCommand();
    }

    /**
     * Gets the exit status of the underlying {@code Process}.
     *
     * @return
     */
    public int getExitStatus() {
        return exitStatus;
    }

    /**
     *
     * @return
     */
    public ExecutionStatus getExecutionStatus() {
        return executionStatus;
    }

    public void setExecutionStatus(ExecutionStatus executionStatus) {
        ExecutionStatus oldStatus = this.executionStatus;
        this.executionStatus = executionStatus;
        notifyExecutionListeners(oldStatus, executionStatus);
    }

    public TerminationReason getTerminationReason() {
        return terminationReason;
    }

    /**
     * <code>true</code> if the underlying process has exited successfully.
     *
     * @return
     */
    public boolean hasCompleted() {
        return ExecutionStatus.TERMINATED.equals(executionStatus) && TerminationReason.COMPLETED_SUCCESSFULY.equals(terminationReason);
    }

    /**
     * <code>true</code> if the underlying process has terminated.
     *
     * @return
     */
    public boolean hasTerminated() {
        return ExecutionStatus.TERMINATED.equals(executionStatus);
    }

    /**
     * <code>true</code> if the underlying process has exited.
     *
     * @return
     */
    public boolean hasExited() {
        return ExecutionStatus.TERMINATED.equals(terminationReason);
    }

    /**
     * Gets anything the underlying process printed into the standard output.
     *
     * @return
     */
    public abstract String getOutput();

    /**
     * Gets anything the underlying process printed into the standard error.
     *
     * @return
     */
    public abstract String getErrorOutput();

    /**
     * Gets all error/standard/exception outputs.
     *
     * @return
     */
    public abstract String getAllOutputs();

    protected void notifyExecutionListeners(ExecutionStatus oldStatus, ExecutionStatus newStatus){
        for (ExecutionListener executionListener : executionListeners) {
            executionListener.statusChanged(oldStatus, newStatus);
        }
    }
    
    /**
     * Gets all messages from exceptions that occurred during execution.
     *
     * @return
     */
    public String[] getExceptionMessages() {
        return exceptionMessages.toArray(new String[exceptionMessages.size()]);
    }

    public boolean hasExceptionMessages() {
        return !exceptionMessages.isEmpty();
    }

    /**
     * Kills the underlying process.
     */
    public abstract void kill();

    /**
     * Executes the command with the given timeout. If timeout is reached, the underlying process is killed.
     *
     * @param timeout the timeout in millisec. If <=0, an infinite timeout is assumed. When the timeout has
     * passed the underlying process will be killed if it still runs. @param block if <code>true</code> the
     * calling thread will wait till the command completes, otherwise the command wi ll be executed in a
     * thread of its own (Still, this <code>CommandExcecution</code> object may be queried regarding process
     * execution data).
     */
    public abstract void executeCommand(long timeout, boolean block);

    /**
     * Executes the command.
     */
    public void executeCommand() {
        executeCommand(0, true);
    }

    /**
     * Equivalent to <code>executeCommand(0, block)</code>
     *
     * @param block if <code>true</code> the calling thread will wait till the command completes, otherwise
     * the command will be executed in a thread of its own (Still, this <code>CommandExcecution</code> object
     * may be queried regarding process execution data).
     * @see executeCommand(long timeout, boolean block)
     */
    public void executeCommand(boolean block) {
        executeCommand(0, block);
    }

    /**
     * Equivalent to <code>executeCommand(timeout, true)</code>
     *
     * @param timeout the timeout in millisec. If <=0, an infinite timeout is assumed. When the timeout has
     * passed the underlying process will be killed if it still runs. @see executeCommand(long timeout,
     * boolean block)
     */
    public void executeCommand(long timeout) {
        executeCommand(timeout, true);
    }

    /**
     * Generates a full description of the execution containing all outputs and errors.
     *
     * @return
     */
    public String describeWithErrors() {
        if (getExitStatus() != 0) {
            StringBuilder sb = new StringBuilder();
            sb.append("Error executing '");
            sb.append(getFullCommand());
            sb.append("'\n");
            if (hasExceptionMessages()) {
                sb.append("Exception: ");
                sb.append(getExceptionMessages()[0]);
            }
            if (getErrorOutput() != null) {
                sb.append("\nError: ");
                sb.append(getErrorOutput());
            }
            return sb.toString();
        }
        return getFullCommand();
    }
}
